Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit 68d433b58e66155dfea02719c6315f6b579c9f4c


Parents : afa2cdb
Author : Ivan <ivan@quad4.io>
Signature : Invalid signer <e46112d44649266d71fe2193e00a4710>, author is <ivan@quad4.io>
Date : 2026-07-16T11:59:43-05:00

feat: update Landlock sandbox with new ABI support and access rights management

Changes
Diff

diff --git a/meshchatx.rsm b/meshchatx.rsm
index b9ecc665..49a354bf 100644
Binary files a/meshchatx.rsm and b/meshchatx.rsm differ

diff --git a/meshchatx/src/backend/landlock_sandbox.py b/meshchatx/src/backend/landlock_sandbox.py
index 355a6ed1..cfe38f0b 100644
--- a/meshchatx/src/backend/landlock_sandbox.py
+++ b/meshchatx/src/backend/landlock_sandbox.py
@@ -6,7 +6,6 @@ from __future__ import annotations
import ctypes
import ctypes.util
-import errno
import logging
import os
import site
@@ -28,18 +27,39 @@ _LANDLOCK_ACCESS_FS_MAKE_SOCK = 1 << 9
_LANDLOCK_ACCESS_FS_MAKE_FIFO = 1 << 10
_LANDLOCK_ACCESS_FS_MAKE_BLOCK = 1 << 11
_LANDLOCK_ACCESS_FS_MAKE_SYM = 1 << 12
+_LANDLOCK_ACCESS_FS_REFER = 1 << 13
+_LANDLOCK_ACCESS_FS_TRUNCATE = 1 << 14
+_LANDLOCK_ACCESS_FS_IOCTL_DEV = 1 << 15
_LANDLOCK_CREATE_RULESET_VERSION = 1 << 0
_LANDLOCK_RULE_PATH_BENEATH = 1
_PR_SET_NO_NEW_PRIVS = 38
-_READ_ACCESS = (
+# ABI v1 filesystem rights. Newer rights are added only when the running ABI
+# supports them, and only granted on paths that already need write or /dev.
+_FS_ACCESS_ABI1 = (
+ _LANDLOCK_ACCESS_FS_EXECUTE
+ | _LANDLOCK_ACCESS_FS_WRITE_FILE
+ | _LANDLOCK_ACCESS_FS_READ_FILE
+ | _LANDLOCK_ACCESS_FS_READ_DIR
+ | _LANDLOCK_ACCESS_FS_REMOVE_DIR
+ | _LANDLOCK_ACCESS_FS_REMOVE_FILE
+ | _LANDLOCK_ACCESS_FS_MAKE_CHAR
+ | _LANDLOCK_ACCESS_FS_MAKE_DIR
+ | _LANDLOCK_ACCESS_FS_MAKE_REG
+ | _LANDLOCK_ACCESS_FS_MAKE_SOCK
+ | _LANDLOCK_ACCESS_FS_MAKE_FIFO
+ | _LANDLOCK_ACCESS_FS_MAKE_BLOCK
+ | _LANDLOCK_ACCESS_FS_MAKE_SYM
+)
+
+_READ_ACCESS_BASE = (
_LANDLOCK_ACCESS_FS_READ_FILE
| _LANDLOCK_ACCESS_FS_READ_DIR
| _LANDLOCK_ACCESS_FS_EXECUTE
)
-_RW_ACCESS = _READ_ACCESS | (
+_RW_ACCESS_BASE = _READ_ACCESS_BASE | (
_LANDLOCK_ACCESS_FS_WRITE_FILE
| _LANDLOCK_ACCESS_FS_REMOVE_DIR
| _LANDLOCK_ACCESS_FS_REMOVE_FILE
@@ -74,6 +94,7 @@ class _LandlockPathBeneathAttr(ctypes.Structure):
("parent_fd", ctypes.c_int32),
]
_pack_ = 1
+ _layout_ = "ms"
def _parse_kernel_version(release: str) -> tuple[int, int, int]:
@@ -118,6 +139,7 @@ def _landlock_env_override() -> bool | None:
_landlock_support_cached: bool | None = None
+_landlock_abi_cached: int | None = None
def _syscall_numbers():
@@ -155,19 +177,74 @@ def _syscall(libc, nr: int, *args):
return rc
-def _probe_landlock_create_ruleset() -> bool:
+def _handled_access_fs_for_abi(abi: int) -> int:
+ """Return handled FS rights for a best-effort sandbox on this ABI.
+
+ Intentionally omits network port rules and IPC scoping so mesh traffic,
+ Unix sockets, and signals keep working. Omits RESOLVE_UNIX for the same
+ reason. Rights we do handle are also granted on RW roots (including /dev).
+ """
+ if abi < 1:
+ return 0
+ handled = _FS_ACCESS_ABI1
+ if abi >= 2:
+ handled |= _LANDLOCK_ACCESS_FS_REFER
+ if abi >= 3:
+ handled |= _LANDLOCK_ACCESS_FS_TRUNCATE
+ if abi >= 5:
+ handled |= _LANDLOCK_ACCESS_FS_IOCTL_DEV
+ return handled
+
+
+def _ruleset_attr_size(abi: int) -> int:
+ """Bytes of landlock_ruleset_attr the running ABI understands."""
+ if abi >= 6:
+ return ctypes.sizeof(_LandlockRulesetAttr)
+ if abi >= 4:
+ return ctypes.sizeof(ctypes.c_uint64) * 2
+ return ctypes.sizeof(ctypes.c_uint64)
+
+
+def _read_access_for_handled(handled: int) -> int:
+ return _READ_ACCESS_BASE & handled
+
+
+def _rw_access_for_handled(handled: int) -> int:
+ access = _RW_ACCESS_BASE
+ if handled & _LANDLOCK_ACCESS_FS_REFER:
+ access |= _LANDLOCK_ACCESS_FS_REFER
+ if handled & _LANDLOCK_ACCESS_FS_TRUNCATE:
+ access |= _LANDLOCK_ACCESS_FS_TRUNCATE
+ if handled & _LANDLOCK_ACCESS_FS_IOCTL_DEV:
+ access |= _LANDLOCK_ACCESS_FS_IOCTL_DEV
+ return access & handled
+
+
+def _probe_landlock_abi() -> int:
+ """Return the Landlock ABI version, or 0 when unavailable."""
+ global _landlock_abi_cached
+ if _landlock_abi_cached is not None:
+ return _landlock_abi_cached
libc = _libc()
nums = _syscall_numbers()
if libc is None or nums is None:
- return False
+ _landlock_abi_cached = 0
+ return 0
create_nr, _, _ = nums
try:
- abi = _syscall(libc, create_nr, 0, 0, _LANDLOCK_CREATE_RULESET_VERSION)
- except OSError as exc:
- if exc.errno in (errno.ENOSYS, errno.EOPNOTSUPP):
- return False
- return False
- return abi >= 1
+ abi = int(_syscall(libc, create_nr, 0, 0, _LANDLOCK_CREATE_RULESET_VERSION))
+ except OSError:
+ _landlock_abi_cached = 0
+ return 0
+ if abi < 1:
+ _landlock_abi_cached = 0
+ return 0
+ _landlock_abi_cached = abi
+ return abi
+
+
+def _probe_landlock_create_ruleset() -> bool:
+ return _probe_landlock_abi() >= 1
def _is_android() -> bool:
@@ -179,6 +256,15 @@ def _is_android() -> bool:
return hasattr(sys, "getandroidapilevel")
+def landlock_abi_version() -> int:
+ """Return the probed Landlock ABI version, or 0 if unsupported."""
+ if sys.platform != "linux" or _is_android():
+ return 0
+ if not _kernel_version_meets_minimum():
+ return 0
+ return _probe_landlock_abi()
+
+
def landlock_kernel_supported() -> bool:
global _landlock_support_cached
if _landlock_support_cached is not None:
@@ -296,20 +382,34 @@ def _collect_rw_roots(
return paths
+def _file_access_from_dir_access(access: int, handled: int) -> int:
+ """Map a directory access mask to rights valid on a non-directory path."""
+ file_bits = _LANDLOCK_ACCESS_FS_READ_FILE | _LANDLOCK_ACCESS_FS_WRITE_FILE
+ if access & _LANDLOCK_ACCESS_FS_EXECUTE:
+ file_bits |= _LANDLOCK_ACCESS_FS_EXECUTE
+ if access & _LANDLOCK_ACCESS_FS_TRUNCATE:
+ file_bits |= _LANDLOCK_ACCESS_FS_TRUNCATE
+ if access & _LANDLOCK_ACCESS_FS_IOCTL_DEV:
+ file_bits |= _LANDLOCK_ACCESS_FS_IOCTL_DEV
+ return file_bits & access & handled
+
+
def _add_path_beneath_rule(
libc,
add_rule_nr: int,
ruleset_fd: int,
path: str,
access: int,
+ handled: int,
) -> None:
if not path or not os.path.exists(path):
return
- effective_access = access
if not os.path.isdir(path):
- effective_access = (
- _LANDLOCK_ACCESS_FS_READ_FILE | _LANDLOCK_ACCESS_FS_WRITE_FILE
- )
+ effective_access = _file_access_from_dir_access(access, handled)
+ else:
+ effective_access = access & handled
+ if effective_access == 0:
+ return
open_flags = os.O_PATH | os.O_CLOEXEC | os.O_RDONLY
try:
fd = os.open(path, open_flags)
@@ -353,13 +453,21 @@ def apply_landlock_sandbox(
logger.warning("Landlock disabled: %s", exc)
return False
- attr = _LandlockRulesetAttr(handled_access_fs=_RW_ACCESS)
+ abi = _probe_landlock_abi()
+ if abi < 1:
+ logger.warning("Landlock disabled: ABI probe failed")
+ return False
+
+ handled = _handled_access_fs_for_abi(abi)
+ read_access = _read_access_for_handled(handled)
+ rw_access = _rw_access_for_handled(handled)
+ attr = _LandlockRulesetAttr(handled_access_fs=handled)
try:
ruleset_fd = _syscall(
libc,
create_nr,
ctypes.byref(attr),
- ctypes.sizeof(attr),
+ _ruleset_attr_size(abi),
0,
)
except OSError as exc:
@@ -368,13 +476,17 @@ def apply_landlock_sandbox(
try:
for root in _collect_read_roots():
- _add_path_beneath_rule(libc, add_rule_nr, ruleset_fd, root, _READ_ACCESS)
+ _add_path_beneath_rule(
+ libc, add_rule_nr, ruleset_fd, root, read_access, handled
+ )
rw_roots = _collect_rw_roots(storage_dir, reticulum_config_dir, log_dir)
public_existing = _existing_dir(public_dir)
if public_existing and public_existing not in rw_roots:
rw_roots.append(public_existing)
for root in rw_roots:
- _add_path_beneath_rule(libc, add_rule_nr, ruleset_fd, root, _RW_ACCESS)
+ _add_path_beneath_rule(
+ libc, add_rule_nr, ruleset_fd, root, rw_access, handled
+ )
_syscall(libc, restrict_nr, ruleset_fd, 0)
except OSError as exc:
logger.warning("Landlock disabled while adding rules: %s", exc)
@@ -390,7 +502,10 @@ def apply_landlock_sandbox(
pass
if landlock_auto_enabled():
- logger.info("Landlock filesystem sandbox enabled (auto-detected on Linux)")
+ logger.info(
+ "Landlock filesystem sandbox enabled (auto-detected on Linux, ABI %s)",
+ abi,
+ )
else:
- logger.info("Landlock filesystem sandbox enabled")
+ logger.info("Landlock filesystem sandbox enabled (ABI %s)", abi)
return True

diff --git a/tests/backend/test_landlock_sandbox.py b/tests/backend/test_landlock_sandbox.py
index 85887a72..63ef1e94 100644
--- a/tests/backend/test_landlock_sandbox.py
+++ b/tests/backend/test_landlock_sandbox.py
@@ -107,3 +107,116 @@ def test_collect_read_roots_includes_interpreter_prefix():
assert any(
prefix == root or prefix.startswith(root.rstrip("/") + "/") for root in roots
), f"prefix {prefix!r} not covered by {roots!r}"
+
+
+def test_handled_access_fs_for_abi_gates_new_rights():
+ abi1 = ll._handled_access_fs_for_abi(1)
+ assert abi1 & ll._LANDLOCK_ACCESS_FS_REFER == 0
+ assert abi1 & ll._LANDLOCK_ACCESS_FS_TRUNCATE == 0
+ assert abi1 & ll._LANDLOCK_ACCESS_FS_IOCTL_DEV == 0
+ assert abi1 & ll._LANDLOCK_ACCESS_FS_WRITE_FILE
+
+ abi2 = ll._handled_access_fs_for_abi(2)
+ assert abi2 & ll._LANDLOCK_ACCESS_FS_REFER
+ assert abi2 & ll._LANDLOCK_ACCESS_FS_TRUNCATE == 0
+
+ abi3 = ll._handled_access_fs_for_abi(3)
+ assert abi3 & ll._LANDLOCK_ACCESS_FS_REFER
+ assert abi3 & ll._LANDLOCK_ACCESS_FS_TRUNCATE
+ assert abi3 & ll._LANDLOCK_ACCESS_FS_IOCTL_DEV == 0
+
+ abi5 = ll._handled_access_fs_for_abi(5)
+ assert abi5 & ll._LANDLOCK_ACCESS_FS_IOCTL_DEV
+ # Network and UNIX-resolve rights stay unhandled on purpose.
+ assert abi5 == ll._handled_access_fs_for_abi(10)
+
+
+def test_rw_access_grants_new_rights_when_handled():
+ handled = ll._handled_access_fs_for_abi(5)
+ read_access = ll._read_access_for_handled(handled)
+ rw_access = ll._rw_access_for_handled(handled)
+ assert read_access & ll._LANDLOCK_ACCESS_FS_TRUNCATE == 0
+ assert read_access & ll._LANDLOCK_ACCESS_FS_IOCTL_DEV == 0
+ assert read_access & ll._LANDLOCK_ACCESS_FS_REFER == 0
+ assert rw_access & ll._LANDLOCK_ACCESS_FS_TRUNCATE
+ assert rw_access & ll._LANDLOCK_ACCESS_FS_IOCTL_DEV
+ assert rw_access & ll._LANDLOCK_ACCESS_FS_REFER
+
+
+def test_ruleset_attr_size_matches_abi():
+ assert ll._ruleset_attr_size(1) == 8
+ assert ll._ruleset_attr_size(3) == 8
+ assert ll._ruleset_attr_size(4) == 16
+ assert ll._ruleset_attr_size(5) == 16
+ assert ll._ruleset_attr_size(6) == 24
+
+
+def test_file_access_includes_truncate_with_write():
+ handled = ll._handled_access_fs_for_abi(5)
+ rw = ll._rw_access_for_handled(handled)
+ file_access = ll._file_access_from_dir_access(rw, handled)
+ assert file_access & ll._LANDLOCK_ACCESS_FS_WRITE_FILE
+ assert file_access & ll._LANDLOCK_ACCESS_FS_TRUNCATE
+ assert file_access & ll._LANDLOCK_ACCESS_FS_IOCTL_DEV
+
+
+@pytest.mark.skipif(sys.platform != "linux", reason="Landlock probe requires Linux")
+def test_landlock_abi_version_on_linux():
+ ll._landlock_abi_cached = None
+ ll._landlock_support_cached = None
+ abi = ll.landlock_abi_version()
+ assert isinstance(abi, int)
+ assert abi >= 0
+ if ll.landlock_kernel_supported():
+ assert abi >= 1
+
+
+@pytest.mark.skipif(
+ sys.platform != "linux" or not ll.landlock_kernel_supported(),
+ reason="Landlock apply requires a supported Linux kernel",
+)
+def test_apply_landlock_preserves_storage_write_and_truncate(tmp_path):
+ """Apply sandbox in a subprocess and confirm RW + truncate still work."""
+ import subprocess
+ import textwrap
+ from pathlib import Path
+
+ storage = tmp_path / "storage"
+ storage.mkdir()
+ script = textwrap.dedent(
+ f"""
+ import os
+ import sys
+ from meshchatx.src.backend.landlock_sandbox import apply_landlock_sandbox
+
+ storage = {str(storage)!r}
+ os.environ["MESHCHAT_LANDLOCK"] = "1"
+ ok = apply_landlock_sandbox(storage_dir=storage, log_dir=storage)
+ if not ok:
+ print("APPLY_FAILED")
+ sys.exit(2)
+ path = os.path.join(storage, "landlock-abi-check.txt")
+ with open(path, "w", encoding="utf-8") as handle:
+ handle.write("hello")
+ with open(path, "w", encoding="utf-8") as handle:
+ handle.write("truncated")
+ with open(path, encoding="utf-8") as handle:
+ data = handle.read()
+ if data != "truncated":
+ print("TRUNCATE_FAILED", repr(data))
+ sys.exit(3)
+ print("OK")
+ """
+ )
+ result = subprocess.run(
+ [sys.executable, "-c", script],
+ cwd=str(Path(__file__).resolve().parents[2]),
+ capture_output=True,
+ text=True,
+ timeout=30,
+ check=False,
+ )
+ if "APPLY_FAILED" in result.stdout:
+ pytest.skip("Landlock could not be applied in this environment")
+ assert result.returncode == 0, (result.stdout, result.stderr)
+ assert "OK" in result.stdout

diff --git a/vendor/lxmfy/lxmfy/landlock_sandbox.py b/vendor/lxmfy/lxmfy/landlock_sandbox.py
index 1082e3fa..60d0fb04 100644
--- a/vendor/lxmfy/lxmfy/landlock_sandbox.py
+++ b/vendor/lxmfy/lxmfy/landlock_sandbox.py
@@ -26,18 +26,39 @@ _LANDLOCK_ACCESS_FS_MAKE_SOCK = 1 << 9
_LANDLOCK_ACCESS_FS_MAKE_FIFO = 1 << 10
_LANDLOCK_ACCESS_FS_MAKE_BLOCK = 1 << 11
_LANDLOCK_ACCESS_FS_MAKE_SYM = 1 << 12
+_LANDLOCK_ACCESS_FS_REFER = 1 << 13
+_LANDLOCK_ACCESS_FS_TRUNCATE = 1 << 14
+_LANDLOCK_ACCESS_FS_IOCTL_DEV = 1 << 15
_LANDLOCK_CREATE_RULESET_VERSION = 1 << 0
_LANDLOCK_RULE_PATH_BENEATH = 1
_PR_SET_NO_NEW_PRIVS = 38
-_READ_ACCESS = (
+# ABI v1 filesystem rights. Newer rights are added only when the running ABI
+# supports them, and only granted on paths that already need write or /dev.
+_FS_ACCESS_ABI1 = (
+ _LANDLOCK_ACCESS_FS_EXECUTE
+ | _LANDLOCK_ACCESS_FS_WRITE_FILE
+ | _LANDLOCK_ACCESS_FS_READ_FILE
+ | _LANDLOCK_ACCESS_FS_READ_DIR
+ | _LANDLOCK_ACCESS_FS_REMOVE_DIR
+ | _LANDLOCK_ACCESS_FS_REMOVE_FILE
+ | _LANDLOCK_ACCESS_FS_MAKE_CHAR
+ | _LANDLOCK_ACCESS_FS_MAKE_DIR
+ | _LANDLOCK_ACCESS_FS_MAKE_REG
+ | _LANDLOCK_ACCESS_FS_MAKE_SOCK
+ | _LANDLOCK_ACCESS_FS_MAKE_FIFO
+ | _LANDLOCK_ACCESS_FS_MAKE_BLOCK
+ | _LANDLOCK_ACCESS_FS_MAKE_SYM
+)
+
+_READ_ACCESS_BASE = (
_LANDLOCK_ACCESS_FS_READ_FILE
| _LANDLOCK_ACCESS_FS_READ_DIR
| _LANDLOCK_ACCESS_FS_EXECUTE
)
-_RW_ACCESS = _READ_ACCESS | (
+_RW_ACCESS_BASE = _READ_ACCESS_BASE | (
_LANDLOCK_ACCESS_FS_WRITE_FILE
| _LANDLOCK_ACCESS_FS_REMOVE_DIR
| _LANDLOCK_ACCESS_FS_REMOVE_FILE
@@ -72,6 +93,7 @@ class _LandlockPathBeneathAttr(ctypes.Structure):
("parent_fd", ctypes.c_int32),
]
_pack_ = 1
+ _layout_ = "ms"
def _parse_kernel_version(release: str) -> tuple[int, int, int]:
@@ -116,6 +138,7 @@ def _landlock_env_override() -> bool | None:
_landlock_support_cached: bool | None = None
+_landlock_abi_cached: int | None = None
def _syscall_numbers():
@@ -143,19 +166,83 @@ def _syscall(libc, nr: int, *args):
return rc
-def _probe_landlock_create_ruleset() -> bool:
+def _handled_access_fs_for_abi(abi: int) -> int:
+ """Return handled FS rights for a best-effort sandbox on this ABI.
+
+ Intentionally omits network port rules and IPC scoping so mesh traffic,
+ Unix sockets, and signals keep working. Omits RESOLVE_UNIX for the same
+ reason. Rights we do handle are also granted on RW roots (including /dev).
+ """
+ if abi < 1:
+ return 0
+ handled = _FS_ACCESS_ABI1
+ if abi >= 2:
+ handled |= _LANDLOCK_ACCESS_FS_REFER
+ if abi >= 3:
+ handled |= _LANDLOCK_ACCESS_FS_TRUNCATE
+ if abi >= 5:
+ handled |= _LANDLOCK_ACCESS_FS_IOCTL_DEV
+ return handled
+
+
+def _ruleset_attr_size(abi: int) -> int:
+ """Bytes of landlock_ruleset_attr the running ABI understands."""
+ if abi >= 6:
+ return ctypes.sizeof(_LandlockRulesetAttr)
+ if abi >= 4:
+ return ctypes.sizeof(ctypes.c_uint64) * 2
+ return ctypes.sizeof(ctypes.c_uint64)
+
+
+def _read_access_for_handled(handled: int) -> int:
+ return _READ_ACCESS_BASE & handled
+
+
+def _rw_access_for_handled(handled: int) -> int:
+ access = _RW_ACCESS_BASE
+ if handled & _LANDLOCK_ACCESS_FS_REFER:
+ access |= _LANDLOCK_ACCESS_FS_REFER
+ if handled & _LANDLOCK_ACCESS_FS_TRUNCATE:
+ access |= _LANDLOCK_ACCESS_FS_TRUNCATE
+ if handled & _LANDLOCK_ACCESS_FS_IOCTL_DEV:
+ access |= _LANDLOCK_ACCESS_FS_IOCTL_DEV
+ return access & handled
+
+
+def _probe_landlock_abi() -> int:
+ """Return the Landlock ABI version, or 0 when unavailable."""
+ global _landlock_abi_cached
+ if _landlock_abi_cached is not None:
+ return _landlock_abi_cached
libc = _libc()
nums = _syscall_numbers()
if libc is None or nums is None:
- return False
+ _landlock_abi_cached = 0
+ return 0
create_nr, _, _ = nums
try:
- abi = _syscall(libc, create_nr, 0, 0, _LANDLOCK_CREATE_RULESET_VERSION)
- except OSError as exc:
- if exc.errno in (errno.ENOSYS, errno.EOPNOTSUPP):
- return False
- return False
- return abi >= 1
+ abi = int(_syscall(libc, create_nr, 0, 0, _LANDLOCK_CREATE_RULESET_VERSION))
+ except OSError:
+ _landlock_abi_cached = 0
+ return 0
+ if abi < 1:
+ _landlock_abi_cached = 0
+ return 0
+ _landlock_abi_cached = abi
+ return abi
+
+
+def _probe_landlock_create_ruleset() -> bool:
+ return _probe_landlock_abi() >= 1
+
+
+def landlock_abi_version() -> int:
+ """Return the probed Landlock ABI version, or 0 if unsupported."""
+ if sys.platform != "linux":
+ return 0
+ if not _kernel_version_meets_minimum():
+ return 0
+ return _probe_landlock_abi()
def landlock_kernel_supported() -> bool:
@@ -274,20 +361,34 @@ def _collect_rw_roots(
return paths
+def _file_access_from_dir_access(access: int, handled: int) -> int:
+ """Map a directory access mask to rights valid on a non-directory path."""
+ file_bits = _LANDLOCK_ACCESS_FS_READ_FILE | _LANDLOCK_ACCESS_FS_WRITE_FILE
+ if access & _LANDLOCK_ACCESS_FS_EXECUTE:
+ file_bits |= _LANDLOCK_ACCESS_FS_EXECUTE
+ if access & _LANDLOCK_ACCESS_FS_TRUNCATE:
+ file_bits |= _LANDLOCK_ACCESS_FS_TRUNCATE
+ if access & _LANDLOCK_ACCESS_FS_IOCTL_DEV:
+ file_bits |= _LANDLOCK_ACCESS_FS_IOCTL_DEV
+ return file_bits & access & handled
+
+
def _add_path_beneath_rule(
libc,
add_rule_nr: int,
ruleset_fd: int,
path: str,
access: int,
+ handled: int,
) -> None:
if not path or not os.path.exists(path):
return
- effective_access = access
if not os.path.isdir(path):
- effective_access = (
- _LANDLOCK_ACCESS_FS_READ_FILE | _LANDLOCK_ACCESS_FS_WRITE_FILE
- )
+ effective_access = _file_access_from_dir_access(access, handled)
+ else:
+ effective_access = access & handled
+ if effective_access == 0:
+ return
open_flags = os.O_PATH | os.O_CLOEXEC | os.O_RDONLY
try:
fd = os.open(path, open_flags)
@@ -335,13 +436,21 @@ def apply_landlock_sandbox(
logger.warning("Landlock disabled: %s", exc)
return False
- attr = _LandlockRulesetAttr(handled_access_fs=_RW_ACCESS)
+ abi = _probe_landlock_abi()
+ if abi < 1:
+ logger.warning("Landlock disabled: ABI probe failed")
+ return False
+
+ handled = _handled_access_fs_for_abi(abi)
+ read_access = _read_access_for_handled(handled)
+ rw_access = _rw_access_for_handled(handled)
+ attr = _LandlockRulesetAttr(handled_access_fs=handled)
try:
ruleset_fd = _syscall(
libc,
create_nr,
ctypes.byref(attr),
- ctypes.sizeof(attr),
+ _ruleset_attr_size(abi),
0,
)
except OSError as exc:
@@ -350,7 +459,9 @@ def apply_landlock_sandbox(
try:
for root in _collect_read_roots(extra_read_paths):
- _add_path_beneath_rule(libc, add_rule_nr, ruleset_fd, root, _READ_ACCESS)
+ _add_path_beneath_rule(
+ libc, add_rule_nr, ruleset_fd, root, read_access, handled
+ )
for root in _collect_rw_roots(
storage_dir,
reticulum_config_dir,
@@ -359,7 +470,9 @@ def apply_landlock_sandbox(
log_dir,
temp_only=temp_only,
):
- _add_path_beneath_rule(libc, add_rule_nr, ruleset_fd, root, _RW_ACCESS)
+ _add_path_beneath_rule(
+ libc, add_rule_nr, ruleset_fd, root, rw_access, handled
+ )
_syscall(libc, restrict_nr, ruleset_fd, 0)
except OSError as exc:
logger.warning("Landlock disabled while adding rules: %s", exc)
@@ -375,9 +488,12 @@ def apply_landlock_sandbox(
pass
if landlock_auto_enabled(config_enabled):
- logger.info("Landlock filesystem sandbox enabled (auto-detected on Linux)")
+ logger.info(
+ "Landlock filesystem sandbox enabled (auto-detected on Linux, ABI %s)",
+ abi,
+ )
else:
- logger.info("Landlock filesystem sandbox enabled")
+ logger.info("Landlock filesystem sandbox enabled (ABI %s)", abi)
return True
@@ -385,7 +501,7 @@ def landlock_status_dict(
*,
active: bool = False,
config_enabled: bool = True,
-) -> dict[str, bool]:
+) -> dict[str, bool | int]:
"""Return a dict describing Landlock availability and state."""
return {
"landlock_kernel_supported": landlock_kernel_supported(),
@@ -393,4 +509,5 @@ def landlock_status_dict(
"landlock_auto_enabled": landlock_auto_enabled(config_enabled),
"landlock_disabled_by_env": landlock_disabled_by_env(),
"landlock_active": active,
+ "landlock_abi_version": landlock_abi_version(),
}

diff --git a/vendor/lxmfy/tests/test_landlock_sandbox.py b/vendor/lxmfy/tests/test_landlock_sandbox.py
index c4f4625f..e00b8793 100644
--- a/vendor/lxmfy/tests/test_landlock_sandbox.py
+++ b/vendor/lxmfy/tests/test_landlock_sandbox.py
@@ -95,3 +95,33 @@ def test_landlock_status_dict():
assert status["landlock_active"] is True
assert "landlock_kernel_supported" in status
assert "landlock_requested" in status
+ assert "landlock_abi_version" in status
+
+
+def test_handled_access_fs_for_abi_gates_new_rights():
+ abi1 = ll._handled_access_fs_for_abi(1)
+ assert abi1 & ll._LANDLOCK_ACCESS_FS_REFER == 0
+ assert abi1 & ll._LANDLOCK_ACCESS_FS_TRUNCATE == 0
+ assert abi1 & ll._LANDLOCK_ACCESS_FS_IOCTL_DEV == 0
+
+ abi5 = ll._handled_access_fs_for_abi(5)
+ assert abi5 & ll._LANDLOCK_ACCESS_FS_REFER
+ assert abi5 & ll._LANDLOCK_ACCESS_FS_TRUNCATE
+ assert abi5 & ll._LANDLOCK_ACCESS_FS_IOCTL_DEV
+ assert abi5 == ll._handled_access_fs_for_abi(10)
+
+
+def test_rw_access_grants_new_rights_when_handled():
+ handled = ll._handled_access_fs_for_abi(5)
+ rw_access = ll._rw_access_for_handled(handled)
+ read_access = ll._read_access_for_handled(handled)
+ assert read_access & ll._LANDLOCK_ACCESS_FS_TRUNCATE == 0
+ assert rw_access & ll._LANDLOCK_ACCESS_FS_TRUNCATE
+ assert rw_access & ll._LANDLOCK_ACCESS_FS_IOCTL_DEV
+ assert rw_access & ll._LANDLOCK_ACCESS_FS_REFER
+
+
+def test_ruleset_attr_size_matches_abi():
+ assert ll._ruleset_attr_size(1) == 8
+ assert ll._ruleset_attr_size(4) == 16
+ assert ll._ruleset_attr_size(6) == 24


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────